package io.kaif.mail;
import static java.util.Arrays.asList;
import java.io.UnsupportedEncodingException;
import java.util.Optional;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.TimeUnit;
import javax.annotation.PostConstruct;
import javax.annotation.PreDestroy;
import javax.mail.internet.AddressException;
import javax.mail.internet.InternetAddress;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.mail.MailException;
import org.springframework.stereotype.Component;
import com.amazonaws.auth.AWSCredentials;
import com.amazonaws.auth.AWSStaticCredentialsProvider;
import com.amazonaws.auth.BasicAWSCredentials;
import com.amazonaws.regions.Regions;
import com.amazonaws.services.simpleemail.AmazonSimpleEmailService;
import com.amazonaws.services.simpleemail.AmazonSimpleEmailServiceClientBuilder;
import com.amazonaws.services.simpleemail.model.Body;
import com.amazonaws.services.simpleemail.model.Content;
import com.amazonaws.services.simpleemail.model.Destination;
import com.amazonaws.services.simpleemail.model.Message;
import com.amazonaws.services.simpleemail.model.SendEmailRequest;
import com.amazonaws.services.simpleemail.model.SendEmailResult;
import com.google.common.base.Charsets;
import com.google.common.util.concurrent.RateLimiter;
import com.google.common.util.concurrent.ThreadFactoryBuilder;
/**
* the bean will be enabled if and only if mail.aws-secret-key present
* <p>
* this is for real production server only. In vagrant, although spring profile set to prod, but it
* set mail.aws-secret-key to <code>false</code> explicitly, spring will treat it is missing.
*/
@Component
@ConditionalOnProperty(prefix = "mail", name = "aws-secret-key")
public class AwsSesMailAgent implements MailAgent {
private static final Logger logger = LoggerFactory.getLogger(AwsSesMailAgent.class);
private AmazonSimpleEmailService client;
private final ExecutorService executor = Executors.newFixedThreadPool(5,
new ThreadFactoryBuilder().setNameFormat("aws-ses-mail-agent-pool-%d").build());
@Autowired
private MailProperties mailProperties;
@Autowired
private MailComposer mailComposer;
//TODO adjust rate limiter if our aws ses mail passed
private final RateLimiter rateLimiter = RateLimiter.create(5.0);
@Override
public MailComposer mailComposer() {
return mailComposer;
}
@PostConstruct
public void afterPropertiesSet() {
AWSCredentials awsSesCredentials = new BasicAWSCredentials(mailProperties.getAwsAccessKey(),
mailProperties.getAwsSecretKey());
this.client = AmazonSimpleEmailServiceClientBuilder.standard()
.withRegion(Regions.US_EAST_1)
.withCredentials(new AWSStaticCredentialsProvider(awsSesCredentials))
.build();
logger.info("mail agent ready, sender:"
+ mailProperties.getAwsSenderAddress()
+ ", access key:"
+ awsSesCredentials.getAWSAccessKeyId());
}
@PreDestroy
public void destroy() throws Exception {
logger.info("shutdowning, wait all mails sent...");
executor.shutdown();
try {
executor.awaitTermination(1, TimeUnit.MINUTES);
} finally {
this.client.shutdown();
}
logger.info("shutdowned.");
}
@Override
public CompletableFuture<Boolean> send(Mail mailMessage) throws MailException {
Message message = new Message();
message.setSubject(new Content(mailMessage.getSubject()).withCharset(Charsets.UTF_8.toString()));
message.setBody(new Body(new Content(mailMessage.getText()).withCharset(Charsets.UTF_8.toString())));
Destination destination = new Destination(asList(mailMessage.getTo()));
Optional.ofNullable(mailMessage.getCc())
.filter(cc -> cc.length > 0)
.ifPresent(cc -> destination.setCcAddresses(asList(cc)));
Optional.ofNullable(mailMessage.getBcc())
.filter(cc -> cc.length > 0)
.ifPresent(cc -> destination.setBccAddresses(asList(cc)));
SendEmailRequest sendEmailRequest = new SendEmailRequest(composeSource(mailMessage).toString(),
destination,
message);
Optional.ofNullable(mailMessage.getReplyTo())
.ifPresent(r -> sendEmailRequest.setReplyToAddresses(asList(r)));
return CompletableFuture.supplyAsync(() -> {
double totalWait = rateLimiter.acquire();
if (totalWait > 5) {
logger.warn("rate limit wait too long: " + totalWait + " seconds");
}
SendEmailResult emailResult = client.sendEmail(sendEmailRequest);
if (logger.isDebugEnabled()) {
logger.debug("sent mail messageId:{}, body:\n{}", emailResult.getMessageId(), mailMessage);
} else {
logger.info("sent mail to {}, messageId:{}", destination, emailResult.getMessageId());
}
return true;
}, executor).handle((result, e) -> {
if (e != null) {
logger.warn("fail send mail to " + destination + ", error:" + e.getMessage());
return false;
}
return true;
});
}
/*
* check spring MimeMessageHelper for how to encode RFC4072
*/
private InternetAddress composeSource(Mail mailMessage) {
try {
if (mailMessage.getFromName() == null) {
return new InternetAddress(mailProperties.getAwsSenderAddress());
} else {
return new InternetAddress(mailProperties.getAwsSenderAddress(),
mailMessage.getFromName(),
Charsets.UTF_8.toString());
}
} catch (AddressException | UnsupportedEncodingException e) {
throw new IllegalArgumentException("source address error", e);
}
}
}